package bmaso.purest;

import static bmaso.purest.UriUtils.queryParameterNamesAndValues;
import static bmaso.purest.UriUtils.uriPathSegments;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.TagSupport;

/**
 * <p>
 * A container tag used to declare REST-associated metainformation about a JSP
 * page. A JSPREST page uses this tag to indicate the URL parameters expected in
 * the request.
 * </p>
 * 
 * <h2>The 'url' Attribute</h2>
 * 
 * <p>
 * The url attribute describes how to extract service parameters from the
 * request URI. For example:
 * </p>
 * 
 * <tt><pre>
 *  &gt;jr:service uri="/flight/{flightno}">
 *    ...
 *  &gt;/jr:service>
 *  </pre></tt>
 * 
 * <p>
 * The tag above indicates that this JSP page serves resources with any URI
 * matching "/flight/<i>flightno</i>", where <i>flightno</i> matches any value.
 * </p>
 * 
 * <p>
 * When the page is evaluated, the tag will extract the <i>flightno</i> value
 * from the inbound URI, and create a request-scoped variable with the name
 * "flightno" equal to the string value of the second particle of the URI. For
 * example, the evaluating the tag with the request URI "/flight/1073" would
 * make the <i>flightno</i> variable have the value "1073".
 * </p>
 * 
 * <h2>The 'queryParameters' attribute</h2>
 * 
 * <p>
 * The queryParameters attribute describes how to extract service parameters from the
 * request query string. For example:
 * </p>
 * 
 * <tt><pre>
 *  &gt;jr:service ... queryParameters="{pageno}">
 *    ...
 *  &gt;/jr:service>
 *  </pre></tt>
 * 
 * <p>
 * The tag above indicates that this JSP page serves resources with that match
 * the required URI and that have a <i>pageno</i> query string that may have
 * any value.  
 * </p>
 * 
 * <p>
 * When the page is evaluated, the tag will extract the <i>pageno</i> value
 * from the inbound URI's query parameters, and create a request-scoped variable
 * with the name "pageno" equal to the string value of the URI's "pageno" query
 * parameter. For
 * example, the evaluating the tag with the request URI "/flight/1073/manifest?pageno=4" would
 * make the <i>pageno</i> variable have the value "4".
 * </p>
 * 
 * <p>
 * Individual query parameters may appear in a comma-separated list. Whitespace before
 * and after the commas will be ignored. For example, the following service requires the
 * inbound URL to include both a <i>pageno</i> and <i>pagesize</i> attribute; each
 * attribute's value must be a sequence of digit characters.
 * </p>
 * 
 * <tt><pre>
 *  &gt;jr:service ... queryParameters="{pageno:[0-9]+},
 *                                      {pagesize:[0-9]+}">
 *    ...
 *  &gt;/jr:service>
 *  </pre></tt>
 *  
 *  <p>
 *  Adding a "?" character to the beginning of the query parameter means the
 *  parameter is optional: if present then it must match the required regex
 *  pattern. If not present then no binding will be created. In the example
 *  below, both the <i>pageno</i> and <i>pagesize</i> query parameter values
 *  are optional.
 *  </p>
 * 
 * <tt><pre>
 *  &gt;jr:service ... queryParameters="?{pageno:[0-9]+},
 *                                      ?{pagesize:[0-9]+}">
 *    ...
 *  &gt;/jr:service>
 *  </pre></tt>
 *  
 */
public class ServiceTag extends TagSupport {
	private PageContext pageContext;
	private String uri;
	private String queryString;
	private String httpHeaders;
	private String cookies;
	private String consumes;
	private MimeTypePattern mimeTypePattern;
	{
		try {
			mimeTypePattern = new MimeTypePattern("*/*");
		} catch(Exception e) {
			//...will never happen...
		}
	}
	private String produces = "text/xml";
	private String methods = "GET, PUT, POST, DELETE";
	private List<SegmentPattern> listSegmentPatterns =
		new LinkedList<SegmentPattern>();
	private Map<String, NameValuePattern> mapQueryStringPatterns =
		new HashMap<String, NameValuePattern>();
	private Map<String, NameValuePattern> mapHttpHeaderPatterns =
		new HashMap<String, NameValuePattern>();
	private Map<String, NameValuePattern> mapCookiePatterns =
		new HashMap<String, NameValuePattern>();
	private List<HttpMethod> listMethods =
		new LinkedList<HttpMethod>();
	{
		listMethods.add(HttpMethod.GET);
		listMethods.add(HttpMethod.PUT);
		listMethods.add(HttpMethod.POST);
		listMethods.add(HttpMethod.DELETE);
	}

	/**
	 * Describes all or part of the path pattern for resources serviced by this
	 * page. The body of this tag is only evaluated if the inbound path matches
	 * this pattern.
	 */
	public String getUri() {
		return uri;
	}

	public void setUri(String uri) {
		this.uri = uri;
		List<String> segments = uriPathSegments(uri);
		
		listSegmentPatterns = new LinkedList<SegmentPattern>();
		for(String segment : segments) {
			listSegmentPatterns.add(new SegmentPattern(segment));
		}
	}
	
	public String getQueryString() {
		return queryString;
	}
	
	public void setQueryString(String queryParameters) {
		this.queryString = queryParameters;
		String[] queryParameterSegments = queryParameters.split("[,\\s]+");
		
		mapQueryStringPatterns = new HashMap<String, NameValuePattern>();
		for(String segment : queryParameterSegments) {
			NameValuePattern nvp = new NameValuePattern(segment);
			mapQueryStringPatterns.put(nvp.getParameterName(), nvp);
		}
	}
	
	public String getHttpHeaders() {
		return httpHeaders;
	}
	
	public void setHttpHeaders(String httpHeaders) {
		this.httpHeaders = httpHeaders;
		String[] httpHeaderSegments = httpHeaders.split("[,\\s]+");
		
		mapHttpHeaderPatterns = new HashMap<String, NameValuePattern>();
		for(String segment : httpHeaderSegments) {
			NameValuePattern nvp = new NameValuePattern(segment);
			mapHttpHeaderPatterns.put(nvp.getParameterName(), nvp);
		}
	}
	
	public String getCookies() {
		return cookies;
	}
	
	public void setCookies(String cookies) {
		this.cookies = cookies;
		String[] cookieSegments = cookies.split("[,\\s]+");
		
		mapCookiePatterns = new HashMap<String, NameValuePattern>();
		for(String segment : cookieSegments) {
			NameValuePattern nvp = new NameValuePattern(segment);
			mapCookiePatterns.put(nvp.getParameterName(), nvp);
		}
	}
	
	public String getMethods() {
		return methods;
	}
	
	public void setMethods(String methods) {
		this.methods = methods.toUpperCase();
		
		listMethods.clear();
		String[] ms = this.methods.split("[\\s,]+");
		for(String m : ms) {
			HttpMethod hm = HttpMethod.valueOf(m);
			listMethods.add(hm);
		}
	}
	
	public String getConsumes() {
		return consumes;
	}
	
	public void setConsumes(String consumes) throws IllegalMIMETypePattern {
		this.consumes = consumes;
		
		this.mimeTypePattern = new MimeTypePattern(consumes);
	}
	
	public String getProduces() {
		return produces;
	}
	
	public void setProduces(String produces) {
		this.produces = produces;
	}
	
	@Override
	public void setPageContext(PageContext pageContext) {
		super.setPageContext(pageContext);
		this.pageContext = pageContext;
	}

	@Override
	public int doStartTag() throws JspException {
		HttpServletRequest request = (HttpServletRequest)pageContext.getRequest();
		if(requestMatchesExpectations(request)) {
			request.setAttribute(RESTTagLibConstants.RESPONSE_GENERATED_REQUEST_ATTR, Boolean.TRUE);
			request.setAttribute(RESTTagLibConstants.RESPONSE_CONTENTTYPE_REQUEST_ATTR, produces);
			
			for(SegmentPattern segPat : listSegmentPatterns) {
				if(segPat.isBinding()) {
					request.setAttribute(segPat.getBindingName(), segPat.getMatchedValue());
				}
				
				for(NameValuePattern nvp : segPat.getMatrixParameterNameValuePatternMap().values()) {
					if(nvp.getMatchedValue() != null) {
						request.setAttribute(nvp.getBindingName(), nvp.getMatchedValue());
					}
				}
			}
			
			for(NameValuePattern nvp : mapQueryStringPatterns.values()) {
				if(nvp.getMatchedValue() != null) {
					request.setAttribute(nvp.getBindingName(), nvp.getMatchedValue());
				}
			}
			
			for(NameValuePattern nvp : mapHttpHeaderPatterns.values()) {
				if(nvp.getMatchedValue() != null) {
					request.setAttribute(nvp.getBindingName(), nvp.getMatchedValue());
				}
			}
			
			for(NameValuePattern nvp : mapCookiePatterns.values()) {
				if(nvp.getMatchedValue() != null) {
					request.setAttribute(nvp.getBindingName(), nvp.getMatchedValue());
				}
			}
			
			return EVAL_BODY_INCLUDE;
		} else {
			request.setAttribute(RESTTagLibConstants.RESPONSE_GENERATED_REQUEST_ATTR, Boolean.FALSE);
			return SKIP_BODY;
		}
	}

	private boolean requestMatchesExpectations(HttpServletRequest request) {
		unbindSegmentPatterns();
		unbindQueryStringPatterns();
		unbindHttpHeaderPatterns();
		unbindCookiePatterns();
		
		return httpMethodMatchExpectations(request) &&
				contentTypeMatchesExpectations(request) &&
				uriSegmentsMatchExpectations(request) &&
				queryStringsMatchExpectations(request) &&
				httpHeadersMatchExpectations(request) &&
				cookiesMatchExpectations(request);
	}
	
	private boolean httpMethodMatchExpectations(HttpServletRequest request) {
		HttpMethod m = HttpMethod.valueOf(request.getMethod());
		return listMethods.contains(m);
	}

	private boolean uriSegmentsMatchExpectations(HttpServletRequest request) {
		String requestUriWithinContext = requestContextUri(request);
		Iterator<String> segments = uriPathSegments(requestUriWithinContext).iterator();
		for(SegmentPattern segPat : listSegmentPatterns) {
			if(!segments.hasNext()) {
				unbindSegmentPatterns();
				return false;
			}
			
			String segment = segments.next();
			if(!segPat.match(segment)) {
				unbindSegmentPatterns();
				return false;
			}
		}
		
		if(segments.hasNext()) {
			unbindSegmentPatterns();
			return false;
		}
		
		return true;
	}

	private String requestContextUri(HttpServletRequest request) {
		if(request.getAttribute(DispatcherTag.CONTEXT_URI_ATTRIBUTE_NAME) != null) {
			return (String)request.getAttribute(DispatcherTag.CONTEXT_URI_ATTRIBUTE_NAME);
		} else {
			return request.getRequestURI().substring(request.getContextPath().length());
		}
	}
	
	private boolean queryStringsMatchExpectations(HttpServletRequest request) {
		Map<String, String> queryParameters =
			queryParameterNamesAndValues(request.getQueryString());
		
		for(Map.Entry<String, String> nameAndValue : queryParameters.entrySet()) {
			NameValuePattern nvp = mapQueryStringPatterns.get(nameAndValue.getKey());
			if(nvp != null) {
				if(!nvp.matches(nameAndValue.getValue())) {
					unbindQueryStringPatterns();
					return false;
				}
			}
		}
		
		for(NameValuePattern nvp : mapQueryStringPatterns.values()) {
			if(!nvp.isOptional() && nvp.getMatchedValue() == null) {
				unbindQueryStringPatterns();
				return false;
			}
		}
		
		return true;
	}
	
	private boolean contentTypeMatchesExpectations(HttpServletRequest request) {
		String contentType = request.getContentType();
		if(contentType != null) {
			return mimeTypePattern.matches(contentType);
		} else {
			return mimeTypePattern.matchesAll();
		}
	}
	
	private boolean httpHeadersMatchExpectations(HttpServletRequest request) {
		Map<String, String> httpHeaders =
			httpHeaderNamesAndValues(request);
		
		for(Map.Entry<String, String> nameAndValue : httpHeaders.entrySet()) {
			NameValuePattern nvp = mapHttpHeaderPatterns.get(nameAndValue.getKey());
			if(nvp != null) {
				if(!nvp.matches(nameAndValue.getValue())) {
					unbindHttpHeaderPatterns();
					return false;
				}
			}
		}
		
		for(NameValuePattern nvp : mapHttpHeaderPatterns.values()) {
			if(!nvp.isOptional() && nvp.getMatchedValue() == null) {
				unbindHttpHeaderPatterns();
				return false;
			}
		}
		
		return true;
	}
		
	private Map<String, String> httpHeaderNamesAndValues(HttpServletRequest request) {
		Map<String, String> m = new HashMap<String, String>();
		
		for(Enumeration e = request.getHeaderNames() ; e.hasMoreElements() ; ) {
			String headerName = (String)e.nextElement();
			m.put(headerName, request.getHeader(headerName));
		}
		
		return m;
	}
	
	private boolean cookiesMatchExpectations(HttpServletRequest request) {
		Map<String, List<String>> cookies =
			cookieNamesAndValues(request);
		
		for(Map.Entry<String, List<String>> nameAndValues : cookies.entrySet()) {
			NameValuePattern nvp = mapCookiePatterns.get(nameAndValues.getKey());
			if(nvp != null) {
				if(!nvp.matchesAny(nameAndValues.getValue())) {
					unbindCookiePatterns();
					return false;
				}
			}
		}
		
		for(NameValuePattern nvp : mapCookiePatterns.values()) {
			if(!nvp.isOptional() && nvp.getMatchedValue() == null) {
				unbindCookiePatterns();
				return false;
			}
		}
		
		return true;
	}	
	
	private Map<String, List<String>> cookieNamesAndValues(HttpServletRequest request) {
		Map<String, List<String>> m = new HashMap<String, List<String>>();
		
		Cookie[] requestCookies =
			request.getCookies() == null ? new Cookie[] {} : request.getCookies();
			
		for(Cookie cookie : requestCookies) {
			List<String> l = m.get(cookie.getName());
			if(l == null) {
				l = new LinkedList<String>();
				m.put(cookie.getName(), l);
			}
			l.add(cookie.getValue());
		}
		
		return m;
	}
	private void unbindSegmentPatterns() {
		for(SegmentPattern segPat : listSegmentPatterns) {
			segPat.reset();
		}
	}
	
	private void unbindQueryStringPatterns() {
		for(NameValuePattern nvp : mapQueryStringPatterns.values()) {
			nvp.reset();
		}
	}
	
	private void unbindHttpHeaderPatterns() {
		for(NameValuePattern nvp : mapHttpHeaderPatterns.values()) {
			nvp.reset();
		}
	}
	
	private void unbindCookiePatterns() {
		for(NameValuePattern nvp : mapCookiePatterns.values()) {
			nvp.reset();
		}
	}
	
	private List<String> getVariableNames() {
		List<String> ret = new LinkedList<String>();
		
		for(SegmentPattern sp : listSegmentPatterns) {
			if(sp.isBinding()) ret.add(sp.getBindingName());
			for(NameValuePattern nvp : sp.getMatrixParameterNameValuePatternMap().values()) {
				ret.add(nvp.getBindingName());
			}
		}
		
		for(NameValuePattern nvp : mapQueryStringPatterns.values()) {
			ret.add(nvp.getBindingName());
		}
		
		for(NameValuePattern nvp : mapHttpHeaderPatterns.values()) {
			ret.add(nvp.getBindingName());
		}
		
		for(NameValuePattern nvp : mapCookiePatterns.values()) {
			ret.add(nvp.getBindingName());
		}
		
		return ret;
	}
	
	/**
	 * For use by the ServiceTagTEI class to declare variables
	 * defined by a tag instance.
	 */
	static List<String> variableNamesFromTagAttributes(
			String uri, String queryString, String httpHeaders, String cookies) {
		ServiceTag tmp = new ServiceTag();
		if(uri != null) tmp.setUri(uri);
		if(queryString != null) tmp.setQueryString(queryString);
		if(httpHeaders != null) tmp.setHttpHeaders(httpHeaders);
		if(cookies != null) tmp.setCookies(cookies);
		
		return tmp.getVariableNames();
	}
}
